package uk.co.q3c.deplan.client.domain.task;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.logging.Logger;

import javax.persistence.Id;
import javax.persistence.Transient;

import uk.co.q3c.deplan.client.domain.AbstractDomainObject;
import uk.co.q3c.deplan.client.domain.DomainKind;
import uk.co.q3c.deplan.client.domain.dag.ChangeInvalidException;
import uk.co.q3c.deplan.client.domain.dag.CycleDetectedException;
import uk.co.q3c.deplan.client.domain.dag.DAG;
import uk.co.q3c.deplan.client.domain.resource.IndividualResource;
import uk.co.q3c.deplan.client.domain.resource.PersistenceReferenceException;
import uk.co.q3c.deplan.client.domain.resource.Resource;
import uk.co.q3c.deplan.client.service.ServiceFactory;
import uk.co.q3c.deplan.client.ui.annotations.Calculated;
import uk.co.q3c.deplan.client.util.Q3DateUtil;

import com.googlecode.objectify.Key;
import com.googlecode.objectify.annotation.Cached;
import com.googlecode.objectify.annotation.Indexed;
import com.googlecode.objectify.annotation.Serialized;
import com.googlecode.objectify.annotation.Unindexed;

/**
 * <b>known subclasses</b><br>
 * 
 * {@link BaseTask}<br> {@link BaseTask}
 * 
 * @author DSowerby 20 Jul 2008 12:07:25
 * 
 */
@Unindexed
@Cached(expirationSeconds = 600)
public class BaseTask extends AbstractDomainObject implements Task, Serializable {
	protected static final Logger logger = Logger.getLogger(BaseTask.class.getName());
	public static final int PROJECT_TASK = 0;
	public static final int SERVICE_TASK = 1;
	public static final int PROJECT = 2;
	public static final int SERVICE = 3;

	private static final long serialVersionUID = 1L;

	private boolean resourcedTask = true;
	protected Date actualFinish;

	protected Date actualStart;
	private Key<Resource> assignedResourceKey;
	protected double assignedUnits;
	protected double duration;
	protected Date plannedFinish;

	protected boolean fixedDuration;

	private Key<BaseTask> key;

	@Id
	@Calculated
	private Long keyId;
	protected int priority = 100;
	private int parentPriority = 0;
	/**
	 * A profile of {@link PlanProfileEntry} mapped to resource and date
	 */
	@Serialized
	private List<PlanDailyProfile> profiles;
	@Indexed
	private int taskType;
	private boolean resourceDriven = true;
	protected String specification;
	protected Date startRemaining;
	private Date targetFinishDate;
	private Date targetStartDate;
	private boolean usingParentPriority = true;
	@Transient
	transient private int tempId;
	protected int workAllocated;
	@Calculated
	protected int workDone;
	@Indexed
	protected int workRemaining;

	protected BaseTask() {
		super();
	}

	public BaseTask(ServiceFactory connection) {
		super(connection);
	}

	/**
	 * Adds the supplied predecessor. Calls with null are logged, but not acted on. Calls with duplicate predecessors are ignored. A
	 * predecessor cannot be a summary task.
	 * 
	 * @param predecessor
	 *            the predecessor to be added
	 * 
	 * @see #addPredecessor(Task)
	 * @param predecessor
	 * @throws PersistenceReferenceException
	 */
	@Override
	public void addPredecessor(Task predecessor) throws PersistenceReferenceException {

		if (predecessor == null) {
			logger.fine("null passed to addPredecessor method of " + toString());
			return;
		}

		if (getKeyId() == null) {
			String msg = "Task " + getName() + " must be saved before a predecessor is assgined to it";
			throw new PersistenceReferenceException(msg);
		}

		if (predecessor.getKeyId() == null) {
			String msg = "Task " + getName() + " must be saved before it is assigned as a predecessor to " + getName();
			throw new PersistenceReferenceException(msg);
		}

		try {
			getDag().addEdge(predecessor.getKey(), getKey());
			// predecessors().add(predecessor);
			firePropertyChange("predecessor", null, predecessor);
		} catch (CycleDetectedException e) {
			// if (successorChainContains(predecessor)) {
			String msg = predecessor.qualifiedName() + "cannot be a predecessor to " + this.qualifiedName() + ". It is already a successor";
			logger.info(msg);
			throw new DependencyLoopException(msg);
		} catch (ChangeInvalidException e) {
			// Exception shouldn't get this far
			logger.severe(e.getMessage());
		}

	}

	/**
	 * Adds the supplied successor. Calls with null are logged, but not acted on. Calls with duplicate successors are ignored. A successor
	 * cannot be a summary task.
	 * 
	 * @param successor
	 *            the successor to be added
	 * 
	 * @see #addSuccessor(Task)
	 * @param successor
	 * @throws PersistenceReferenceException
	 */
	@Override
	public void addSuccessor(Task successor) throws PersistenceReferenceException {

		if (successor == null) {
			logger.fine("null passed to addsuccessor method of " + toString());
			return;
		}

		if (getKeyId() == null) {
			String msg = "Task " + getName() + " must be saved before a successor is assigned to it";
			throw new PersistenceReferenceException(msg);
		}

		if (successor.getKeyId() == null) {
			String msg = "Task " + getName() + " must be saved before it is assigned as a successor to " + getName();
			throw new PersistenceReferenceException(msg);
		}

		try {
			getDag().addEdge(getKey(), successor.getKey());
			successors().add(successor);
			firePropertyChange("successor", null, successor);
		} catch (CycleDetectedException e) {
			// if (successorChainContains(successor)) {
			String msg = successor.qualifiedName() + "cannot be a successor to " + this.qualifiedName() + ". It is already a successor";
			logger.info(msg);
			throw new DependencyLoopException(msg);
		} catch (ChangeInvalidException e) {
			// Exception shouldn't get this far
			logger.severe(e.getMessage());
		}

	}

	public void addToWorkDone(int workDone, IndividualResource resource, Date date) throws PersistenceReferenceException {
		if (resource == null) {
			logger.info("addToWorkDone called with null resource");
			return;
		}

		if (date == null) {
			logger.info("addToWorkDone called with null date");
			return;

		}

		PlanDailyProfile profile = getProfile(resource);
		if (profile == null) {
			profile = createProfile(resource);
		}

		PlanProfileEntry ppe = profile.getEntry(date);
		if (ppe == null) {
			ppe = createEntry(profile, date, resource);
		}
		ppe.setWorkDone(ppe.getWorkDone() + workDone);
		int oldValue = this.workDone;
		this.workDone += workDone;
		firePropertyChange("workDone", oldValue, this.workDone);

	}

	@Override
	public void afterLoad(ServiceFactory connection) {
		super.afterLoad(connection);
		if (profiles == null) {
			profiles = new ArrayList<PlanDailyProfile>();
		}
	}

	/**
	 * Allocates <code>workAllocated</code> to this task - adds this to the work already allocated as opposed to
	 * {@link #setWorkAllocated(int)} which just does the usual setter thing. If this is the first work to be allocated, call
	 * {@link #setStartRemaining(Date)} with new start date. If the plannedFinish of this allocation is later than the current plannedFinish
	 * date move the plannedFinish date.
	 * 
	 * @param date
	 *            and time date the work is being done
	 * @param workAllocated
	 *            the amount of work planned to be done (in minutes)
	 * @throws PersistenceReferenceException
	 */
	public void allocateWork(int allocation, StartFinishTimes sft) throws PersistenceReferenceException {

		this.workAllocated = this.workAllocated + allocation;
		// removed this would cause a discrepancy between resources and tasks
		// if (=this.workAllocated > workRemaining) {
		// =this.workAllocated = workRemaining;
		// }

		// only set the startRemaining if was nulled - this is done by
		// IndividualResource as part of the levelling process to indicate that
		// the remainder of the task hasn't been planned
		if (startRemaining == null) {
			setStartRemaining(new Date(sft.start));
		}
		if (getPlannedFinish().getTime() < sft.finish) {
			setPlannedFinish(new Date(sft.finish));
		}
	}

	public String assignmentDescription() {

		return " resource=" + getAssignedUnits() + " x " + getAssignedResource();
	}

	/**
	 * Assigns a resource to this task, at a given level of work
	 * 
	 * @param resource
	 *            the resource to do the work
	 * @param allocation
	 *            the amount of work to do expressed as a percentage of working time
	 * @throws PersistenceReferenceException
	 */
	public void assignResource(Resource resource, double units) throws PersistenceReferenceException {

		setAssignedUnits(units);
		setAssignedResource(resource);

	}

	/**
	 * Attaches the supplied task as a sub-task. If <code>task</code> is already a subTask, the call is ignored, as duplicate subTasks are
	 * not allowed.
	 * 
	 * @param task
	 * @throws ChangeInvalidException
	 * @throws CycleDetectedException
	 * @throws PersistenceReferenceException
	 */
	void attachSubTask(Task task) throws CycleDetectedException, ChangeInvalidException, PersistenceReferenceException {
		Key<? extends Task> subTaskKey = task.getKey();
		connection.taskService()
				.getHierarchy()
				.setParentKeyFor(subTaskKey, this.getKey());

	}

	/**
	 * This should only be called from {@link Resource#bookEffort()} to ensure that the resource doesn't get over booked. The effort wanted
	 * is added to the profile for the specified resource on the specified date
	 * 
	 * @param resource
	 * @param date
	 * @param effortWanted
	 * @throws PersistenceReferenceException
	 */
	public void bookEffort(IndividualResource resource, Date date, int effortWanted) throws PersistenceReferenceException {
		PlanDailyProfile profile = getProfile(resource);
		if (profile == null) {
			profile = createProfile(resource);
		}
		PlanProfileEntry ppe = profile.getEntry(date);
		if (ppe == null) {
			ppe = createEntry(profile, date, resource);
		}
		ppe.bookEffort(effortWanted);
		firePropertyChange("workPlanned", Integer.toString(effortWanted), " booked on " + date.toString());

	}

	@Override
	public void clearLevelling() {
		setStartRemaining(null);
		setWorkAllocated(0);
		if (plannedFinish == null) {
			plannedFinish = new Date();
		}
		this.plannedFinish.setTime(0);
	}

	/**
	 * @param profile
	 * @param time
	 * @return
	 */
	private PlanProfileEntry createEntry(PlanDailyProfile profile, Date date, IndividualResource resource) {
		PlanProfileEntry ppe = new PlanProfileEntry(getConnection(), Q3DateUtil.dateToKey(date));
		ppe.setDate(date);
		profile.addEntry(ppe);
		return ppe;
	}

	/**
	 * @param resource
	 * @return
	 */
	private PlanDailyProfile createProfile(IndividualResource resource) {
		PlanDailyProfile profile = new PlanDailyProfile(resource);
		profiles.add(profile);
		return profile;

	}

	@Override
	public String DAGLabel() {
		return getKind().toString() + ":" + this.keyId.toString();
	}

	/**
	 * 
	 * @throws CycleDetectedException
	 * @throws ChangeInvalidException
	 * @throws PersistenceReferenceException
	 * @see uk.co.q3c.deplan.server.domain.AbstractPersistedObjectView.AbstractPersistedObject#dereference()
	 */
	@Override
	public void dereference() throws CycleDetectedException, ChangeInvalidException, PersistenceReferenceException {
		dereferenceDependencies();
		if (getParentTaskKey() != null) {
			setParentTask(null);
		}
	}

	private void dereferenceDependencies() {

		try {
			connection.taskService()
					.getDag()
					.removeAllConnnections(this.getKey());
		} catch (PersistenceReferenceException e) {
			// do nothing - if it hasn't been saved it cannot be referenced anywhere
		}

	}

	void detachSubTask(Task task) {

		try {
			getSubTasks().remove(task);
			firePropertyChange("detachSubTask", task, null);
		} catch (PersistenceReferenceException e) {
			// ignore - cannot have subtasks if it hasn't been saved
		}

	}

	/**
	 * if the task has
	 */
	@Override
	public Date finish() throws PersistenceReferenceException {

		if (plannedFinish == null) {
			plannedFinish = new Date();
			plannedFinish.setTime(0);
		}
		Date d = new Date();
		d.setTime(plannedFinish.getTime());
		return d;
	}

	@Override
	public long finishTime() throws PersistenceReferenceException {

		return finish().getTime();
	}

	@Override
	public void fireTestProperty() {
		firePropertyChange("_test", null, null);

	}

	public Date getActualFinish() {

		return actualFinish;
	}

	public Date getActualStart() {

		return actualStart;
	}

	public Resource getAssignedResource() {
		if (assignedResourceKey == null) {
			return null;
		}
		return connection.resourceService()
				.getObjectFromKey(assignedResourceKey);
	}

	public Key<Resource> getAssignedResourceKey() {
		return assignedResourceKey;
	}

	public double getAssignedUnits() {

		return assignedUnits;
	}

	@Override
	public String getCounterGroup() {
		return "task";
	}

	public DAG<Key<? extends Task>> getDag() {
		return getConnection().taskService()
				.getDag();
	}

	@Override
	public double getDuration() {
		return duration;
	}

	public Date getPlannedFinish() throws PersistenceReferenceException {

		return finish();
	}

	@Override
	public Key<? extends Task> getKey() throws PersistenceReferenceException {
		if (getKeyId() == null) {
			throw new PersistenceReferenceException(this.getName() + " has not been saved and cannot therefore be used as a reference");
		}
		if (key == null) {
			key = connection.taskService()
					.keyFor(this);
		}
		return key;
	}

	public Long getKeyId() {
		return keyId;
	}

	@Override
	public DomainKind getKind() {
		return DomainKind.AbstractTask;
	}

	@Override
	public BaseTask getParentTask() throws PersistenceReferenceException {
		Key<? extends Task> key = connection.taskService()
				.getHierarchy()
				.parentKeyFor(this.getKey());
		if (key == null) {
			return null;
		} else {
			return (BaseTask) connection.taskService()
					.getObjectFromKey(key);
		}
	}

	@SuppressWarnings("unchecked")
	public Key<BaseTask> getParentTaskKey() throws PersistenceReferenceException {
		Key<? extends Task> parentKey = connection.taskService()
				.getHierarchy()
				.parentKeyFor(this.getKey());
		return (Key<BaseTask>) parentKey;
	}

	/**
	 * Returns a copy of predecessors - the list is a copy but the contained tasks are the same references in the copy as in the original
	 * 
	 * @return
	 * @throws PersistenceReferenceException
	 */
	@Override
	public ArrayList<Task> getPredecessors() throws PersistenceReferenceException {

		ArrayList<Task> copy = new ArrayList<Task>();
		copy.addAll(predecessors());
		return copy;
	}

	@Override
	public int getPriority() {
		return usingParentPriority ? parentPriority : priority;
	}

	/**
	 * Returns the profile for the given resource, or a new one if none exists. Any newly created profile is also added to {@link #profiles}
	 * 
	 * @param resource
	 * @return
	 * @throws PersistenceReferenceException
	 */
	public PlanDailyProfile getProfile(IndividualResource resource) throws PersistenceReferenceException {
		PlanDailyProfile result = null;
		for (PlanDailyProfile pdp : profiles) {
			if (pdp.getResourceKey()
					.equals(resource.getKey())) {
				result = pdp;
			}
		}
		return result;
	}

	public List<PlanDailyProfile> getProfiles() {

		return profiles;
	}

	@Override
	public String getSpecification() {
		return specification;
	}

	public Date getStart() throws PersistenceReferenceException {

		return start();
	}

	/**
	 * @return
	 */
	@Override
	public Date getStartRemaining() {
		return startRemaining;
	}

	/**
	 * Returns list of subtasks.
	 * 
	 * @return
	 * @throws PersistenceReferenceException
	 */
	public List<? extends Task> getSubTasks() throws PersistenceReferenceException {
		// if (subTasks == null) {
		// subTasks = connection.taskService()
		// .subTasksOf(this);
		// }
		// return subTasks;
		return connection.taskService()
				.subTasksOf(this);
	}

	private List<Key<? extends Task>> getSubTasksKeys() throws PersistenceReferenceException {
		return connection.taskService()
				.subTasksKeysFor(this);
	}

	@Override
	public List<Task> getSuccessors() throws PersistenceReferenceException {

		List<Task> copy = new ArrayList<Task>();
		copy.addAll(successors());
		return copy;
	}

	@Override
	public Date getTargetFinishDate() {
		return new Date(targetFinishDate.getTime());
	}

	@Override
	public Date getTargetStartDate() {
		return new Date(targetStartDate.getTime());
	}

	@Override
	public long getTaskId() {
		Long r = getKeyId();
		if (r == null) {
			return 0;
		}
		return r;
	}

	public int getTempId() {
		return tempId;
	}

	/**
	 * Gets total work value in minutes, work done + work remaining
	 * 
	 * @return
	 */
	public int getTotalWork() {

		return workDone + workRemaining;
	}

	public int getWorkAllocated() {

		return workAllocated;
	}

	public int getWorkDone() {

		return workDone;
	}

	/**
	 * Returns the work planned for the specified resource on the specified day.
	 * 
	 * @param resource
	 * @param dateIndex
	 * @return
	 * @throws PersistenceReferenceException
	 */
	public int getWorkPlanned(IndividualResource resource, Date date) throws PersistenceReferenceException {
		PlanDailyProfile profile = getProfile(resource);
		if (profile == null) {
			return 0;
		}
		PlanProfileEntry ppe = profile.getEntry(date);
		if (ppe == null) {
			return 0;
		}
		return ppe.getWorkPlanned();
	}

	@Override
	public int getWorkRemaining() {

		return workRemaining;
	}

	@Override
	public boolean hasAncestor(Task task) throws PersistenceReferenceException {

		if (getParentTask() == null) {
			return false;
		}

		if (getParentTask() == task) {
			return true;
		}
		return getParentTask().hasAncestor(task);
	}

	@Override
	public boolean hasDependencies() {
		return true;
	}

	public boolean hasPredecessor(Task pred) {
		try {
			return connection.taskService()
					.getDag()
					.nodeHasPredecessor(this.getKey(), pred.getKey());
		} catch (PersistenceReferenceException e) {
			return false;
		}

	}

	public boolean hasPredecessors() {

		try {
			return predecessors().size() > 0;
		} catch (PersistenceReferenceException e) {
			return false;
		}
	}

	public boolean hasSubTask(Task task) throws PersistenceReferenceException {
		if (task == null) {
			return false;
		}
		List<Key<? extends Task>> keys = getSubTasksKeys();
		try {
			return keys.contains(task.getKey());
		} catch (PersistenceReferenceException e) {
			return false;
		}

	}

	/**
	 * Returns true if this task has sub tasks.
	 * 
	 * @return
	 */
	public boolean hasSubTasks() {

		try {
			return !getSubTasks().isEmpty();
		} catch (PersistenceReferenceException e) {
			return false;
		}
	}

	public boolean hasSuccessor(Task succ) {

		try {
			return connection.taskService()
					.getDag()
					.nodeHasSuccessor(getKey(), succ.getKey());
		} catch (PersistenceReferenceException e) {
			return false;
		}
	}

	public int indexOf(Task task) {
		try {
			return getSubTasks().indexOf(task);
		} catch (PersistenceReferenceException e) {
			return -1;
		}
	}

	public boolean isComplete() {

		return workRemaining <= 0;
	}

	public boolean isFixedDuration() {

		return fixedDuration;
	}

	/**
	 * Signifies that sufficient resource has been allocated to complete the remaining work
	 * 
	 * @return true if all necessary work has been allocated
	 */
	@Override
	public boolean isFullyAllocated() {

		return workRemaining <= workAllocated;
	}

	public boolean isProject() {
		return taskType == PROJECT;
	}

	@Override
	public boolean isResourceDriven() {
		return resourceDriven;
	}

	public long lastPredecessorFinish() throws PersistenceReferenceException {

		long maxFinish = 0;
		for (Task task : predecessors()) {
			long f = task.finishTime();
			if (f > maxFinish) {
				maxFinish = f;
			}
		}

		return maxFinish;
	}

	@Override
	public void moveDependenciesTo(Task targetTask) throws PersistenceReferenceException {

		targetTask.predecessors()
				.addAll(this.predecessors());
		targetTask.successors()
				.addAll(this.successors());
	}

	/**
	 * Moves the <code>selectedTask</code> down one in subTasks list
	 * 
	 * @param selectedTask
	 * @throws PersistenceReferenceException
	 */
	public void orderDown(Task selectedTask) throws PersistenceReferenceException {
		Key<? extends Task> selectedTaskKey = selectedTask.getKey();
		connection.taskService()
				.getHierarchy()
				.orderDown(getKey(), selectedTaskKey);

	}

	/**
	 * Moves the <code>selectedTask</code> up one in subTasks list
	 * 
	 * @param selectedTask
	 * @throws PersistenceReferenceException
	 */
	public void orderUp(Task selectedTask) throws PersistenceReferenceException {
		Key<? extends Task> selectedTaskKey = selectedTask.getKey();
		connection.taskService()
				.getHierarchy()
				.orderUp(getKey(), selectedTaskKey);
	}

	/**
	 * See {@link #setDiscarded(boolean)}. When a call is made to this method, the <code>discarded</code> and <code>discardedDate</code> are
	 * set for <code>this</code>, but {@link PersistedObject#dereference()} is not called to avoid reflecting the change back, and the link
	 * between parent and subtask. The call is passed to all subtasks.
	 * 
	 * @throws PersistenceReferenceException
	 * 
	 * @see uk.co.q3c.deplan.client.domain.task.Task#parentDiscarded(boolean, java.util.Date)
	 */
	@Override
	public void parentDiscarded(boolean discarded, Date date) throws PersistenceReferenceException {
		if (discarded) {
			dereferenceDependencies();
		}
		this.discarded = discarded;
		if (this.discardedDate == null) {
			this.discardedDate = new Date();
		}
		this.discardedDate.setTime(date.getTime());
		for (Task task : getSubTasks()) {
			task.parentDiscarded(discarded, getDiscardedDate());
		}

	}

	//
	// /**
	// * Used to notify subTasks when their parent has changed property, in keeping with the idea that a subTask with priority==0 uses its
	// * parent's priority. This is so property changes can be fired from the subTasks to update any displays which are listening. Note that
	// * the oldValue parameter in firePropertyChange is fudged to -999 because an unchanged value won't be fired
	// *
	// * @throws PersistenceReferenceException
	// */
	// protected void parentPriorityChanged() throws PersistenceReferenceException {
	// if (this.priority == 0) {
	// firePropertyChange("priority", -999, 0);
	// }
	// }

	/**
	 * Calls inherited method then if the priority of this task is 0, cascades the notification to subTasks
	 */
	public void parentPriorityChanged() throws PersistenceReferenceException {
		// super.parentPriorityChanged();

		if (this.priority == 0) {
			for (Task task : getSubTasks()) {
				BaseTask at = (BaseTask) task;
				at.parentPriorityChanged();
			}
		}
	}

	/**
	 * Returns true if any task in the chain of predecessors contains the proposed successor. This is used to prevent a loop where a
	 * successor is also a predecessor of the same task.
	 * 
	 * @param proposedSuccessor
	 * @return
	 * @throws PersistenceReferenceException
	 */
	@Override
	public boolean predecessorChainContains(Task proposedSuccessor) throws PersistenceReferenceException {

		if (predecessors().contains(proposedSuccessor)) {
			return true;
		}
		for (Task predecessor : predecessors()) {
			if (predecessor instanceof Task) {
				Task atp = predecessor;
				if (atp.predecessorChainContains(proposedSuccessor)) {
					return true;
				}
			}

		}
		return false;
	}

	/**
	 * This retrieves the predecessors from persistence
	 * 
	 * @return
	 * @throws PersistenceReferenceException
	 */
	@Override
	public List<Task> predecessors() throws PersistenceReferenceException {
		return connection.taskService()
				.predecessorsOf(this);
	}

	/**
	 * returns true if all predecessors have been completed
	 * 
	 * @return
	 * @throws PersistenceReferenceException
	 */
	public boolean predecessorsAllocated() throws PersistenceReferenceException {

		boolean result = true;
		for (Task t : predecessors()) {
			if (!t.isFullyAllocated()) {
				result = false;
				break;
			}
		}
		return result;
	}

	public String predecessorsString() throws PersistenceReferenceException {

		StringBuffer buf = new StringBuffer();
		for (int i = 0; i < predecessors().size(); i++) {
			Task task = predecessors().get(i);
			if (i != 0) {
				buf.append(",");
			}
			buf.append(task.getTaskId());
		}
		return buf.toString();
	}

	public int profileCount() {
		return profiles.size();
	}

	@Override
	public String qualifiedName() {
		try {
			return getParentTask().qualifiedName() + "." + getName();
		} catch (Exception e) {
			return getName();
		}

	}

	@Override
	public void removePredecessor(Task predecessor) {
		try {
			boolean removed = connection.taskService()
					.getDag()
					.removeEdge(predecessor.getKey(), getKey());
			if (removed) {
				firePropertyChange("predecessor", predecessor, null);
			}
		} catch (PersistenceReferenceException e) {
			// do nothing
			// if either does not have a key then link could not have been made
		}

	}

	public void removeSubtask(Task task) {

		try {
			getSubTasks().remove(task);
		} catch (PersistenceReferenceException e) {
			// ignore - cannot have subtasks if it hasn't been saved

		}
		firePropertyChange("subTask", task, null);

	}

	@Override
	public void removeSuccessor(Task successor) {
		try {
			boolean removed = connection.taskService()
					.getDag()
					.removeEdge(getKey(), successor.getKey());
			if (removed) {
				firePropertyChange("successor", successor, null);
			}
		} catch (PersistenceReferenceException e) {
			// do nothing
			// if either does not have a key then link could not have been made
		}
	}

	// /**
	// * /** Pass this object to {@link TaskStatusReport#report}
	// *
	// * @throws PersistenceReferenceException
	// */
	// @Override
	// public void reportStatus(TaskStatusReport taskStatusReport) throws PersistenceReferenceException {
	// taskStatusReport.analyse(this);
	//
	// }

	/**
	 * Pass this object to {@link TaskStatusReport#report}, then calls reportStatus on all subTasks
	 * 
	 * @throws PersistenceReferenceException
	 */
	@Override
	public void reportStatus(TaskStatusReport taskStatusReport) throws PersistenceReferenceException {

		taskStatusReport.analyse(this, connection);
		for (Task task : getSubTasks()) {
			task.reportStatus(taskStatusReport);
		}

	}

	@Override
	public Task root() throws PersistenceReferenceException {

		if (getParentTask() == null) {
			return this;
		}

		BaseTask parent = getParentTask();
		Task result = null;
		while (parent != null) {
			result = parent;
			parent = parent.getParentTask();
		}
		return result;
	}

	@Override
	public void save() {
		connection.taskService()
				.save(this);

	}

	@Override
	public void setActualFinish(Date actualFinish) {

		Date oldValue = null;

		if (this.actualFinish == null) {
			this.actualFinish = new Date();
		} else {
			oldValue = new Date(this.actualFinish.getTime());
		}

		if (actualFinish == null) {
			this.actualFinish = null;
		} else {
			this.actualFinish.setTime(actualFinish.getTime());
		}
		firePropertyChange("actualFinish", oldValue, this.actualFinish);

	}

	@Override
	public void setActualStart(Date actualStart) {

		Date oldValue = null;

		if (this.actualStart == null) {
			this.actualStart = new Date();
		} else {
			oldValue = new Date(this.actualStart.getTime());
		}

		if (actualStart == null) {
			this.actualStart = null;
		} else {
			this.actualStart.setTime(actualStart.getTime());
		}
		firePropertyChange("actualStart", oldValue, this.actualStart);
	}

	/**
	 * Sets the assigned resource as specified
	 * 
	 * @param assignedResource
	 * @throws PersistenceReferenceException
	 */
	@SuppressWarnings("unchecked")
	public void setAssignedResource(Resource assignedResource) throws PersistenceReferenceException {
		Key<Resource> oldValue = assignedResourceKey;
		if (assignedResource == null) {
			assignedResourceKey = null;
		} else {
			this.assignedResourceKey = (Key<Resource>) assignedResource.getKey();
		}

		firePropertyChange("assignedResource", oldValue, assignedResourceKey);
	}

	public void setAssignedResourceKey(Key<Resource> assignedResourceKey) {
		this.assignedResourceKey = assignedResourceKey;
	}

	public void setAssignedUnits(double assignedUnits) {

		double oldValue = this.assignedUnits;
		this.assignedUnits = assignedUnits;
		firePropertyChange("assignedUnits", oldValue, this.assignedUnits);
	}

	/**
	 * This method follows the normal pattern of setting the <code>discarded</code> and <code>discardedDate</code>, in turn calling
	 * {@link PersistedObject#dereference()}, to remove all references to this object. As it is intended that a parent task should keep its
	 * subtasks with it when discarded (that is, the subtasks are discarded as well), a call is made to all children to
	 * {@link Task#parentDiscarded(boolean, Date)}
	 * 
	 * @throws CycleDetectedException
	 * @throws PersistenceReferenceException
	 * 
	 * @see uk.co.q3c.deplan.server.domain.AbstractPersistedObjectView.AbstractPersistedObject#setDiscarded(boolean)
	 */
	@Override
	public void setDiscarded(boolean discarded) throws CycleDetectedException, ChangeInvalidException, PersistenceReferenceException {
		super.setDiscarded(discarded);
		if (discarded) {
			if (getParentTaskKey() != null) {
				connection.taskService()
						.getHierarchy()
						.removeEdge(getParentTaskKey(), this.getKey());
			}
		}

		// This would remove link to parent but shuold not
		for (Task task : getSubTasks()) {
			task.parentDiscarded(discarded, getDiscardedDate());
		}
	}

	// /**
	// * This method follows the normal pattern of setting the <code>discarded</code> and <code>discardedDate</code>, in turn calling
	// * {@link PersistedObject#dereference()}, to remove all references to this object. As it is intended that a parent task should keep
	// its
	// * subtasks with it when discarded (that is, the subtasks are discarded as well), a call is made to all children to
	// * {@link Task#parentDiscarded(boolean, Date)}
	// *
	// * @throws CycleDetectedException
	// * @throws PersistenceReferenceException
	// *
	// * @see uk.co.q3c.deplan.server.domain.AbstractPersistedObjectView.AbstractPersistedObject#setDiscarded(boolean)
	// */
	// @Override
	// public void setDiscarded(boolean discarded) throws CycleDetectedException, ChangeInvalidException, PersistenceReferenceException {
	// super.setDiscarded(discarded);
	//
	// // setParentTask(null);
	// }

	public void setDuration(double duration) {

		double oldValue = this.duration;
		this.duration = duration;
		firePropertyChange("duration", oldValue, this.duration);
	}

	public void setPlannedFinish(Date finish) {

		Date oldValue = null;

		if (this.plannedFinish == null) {
			this.plannedFinish = new Date();
		} else {
			oldValue = new Date(this.plannedFinish.getTime());
		}

		if (finish == null) {
			this.plannedFinish = null;
		} else {
			this.plannedFinish.setTime(finish.getTime());
		}
		firePropertyChange("plannedFinish", oldValue, this.plannedFinish);
	}

	@Override
	public void setFixedDuration(boolean fixedDuration) {
		if (fixedDuration) {
			throw new RuntimeException("Fixed duration not yet implemented");
		}
		boolean oldValue = this.fixedDuration;
		this.fixedDuration = fixedDuration;
		firePropertyChange("fixedDuration", oldValue, this.fixedDuration);
	}

	public void setKeyId(Long keyId) {
		this.keyId = keyId;
	}

	/**
	 * If this parentTask has this object as an ancestor this call is ignored, otherwise the parentTask is set.
	 * 
	 * @param newParentTask
	 * @throws CycleDetectedException
	 * @throws ChangeInvalidException
	 * @throws PersistenceReferenceException
	 */
	@SuppressWarnings("unchecked")
	@Override
	public void setParentTask(BaseTask newParentTask) throws CycleDetectedException, ChangeInvalidException, PersistenceReferenceException {
		if (newParentTask == null) {
			setParentTaskKey(null);
		} else {
			setParentTaskKey((Key<BaseTask>) newParentTask.getKey());
		}
	}

	public void setParentTaskKey(Key<BaseTask> parentTaskKey) throws CycleDetectedException, ChangeInvalidException, PersistenceReferenceException {
		Key<BaseTask> oldValue = getParentTaskKey();
		Key<? extends Task> thisKey = getKey();
		connection.taskService()
				.getHierarchy()
				.setParentKeyFor(thisKey, parentTaskKey);

		firePropertyChange("parentTask", oldValue, this.getParentTaskKey());
	}

	/**
	 * Sets priority for this task. For backward compatibility, setting priority to 0 may not actually change priority - it behaves the same
	 * way as setting {@link #usingParentPriority} to true. Equally setting priority to > 0 will cause {@link #usingParentPriority} to be
	 * set false. This is an open issue http://code.google.com/p/deplan/issues/detail?id=83
	 */

	@Override
	public void setPriority(int priority) throws PersistenceReferenceException {
		int oldValue = this.getPriority();
		if (priority < 0) {
			priority = 0;
		}
		if (priority == 0) {
			setUsingParentPriority(true);
			return;
		}
		if (priority > 0) {
			this.priority = priority;
			setUsingParentPriority(false);
			firePropertyChange("priority", oldValue, this.getPriority());
		}
	}

	public void setProfiles(List<PlanDailyProfile> profiles) {
		this.profiles = profiles;
	}

	public void setProject() {
		this.taskType = PROJECT;
	}

	public void setResourceDriven(boolean resourceDriven) {
		this.resourceDriven = resourceDriven;
	}

	@Override
	public void setSpecification(String specification) {

		String oldValue = this.specification;
		this.specification = specification;
		firePropertyChange("specification", oldValue, this.specification);

	}

	/**
	 * Date on remaining activity or work is planned to take place - if some work has already been done, this is the first date the
	 * remaining work will be undertaken. Returns null if the task is complete or work is not planned. The latter could be because no
	 * resource has been assigned (or has been assigned with 0 units), or because it is dependent on a task which cannot complete.
	 */
	@Override
	public void setStartRemaining(Date startRemaining) {

		Date oldValue = null;

		if (this.startRemaining == null) {
			this.startRemaining = new Date();
		} else {
			oldValue = new Date(this.startRemaining.getTime());
		}

		if (startRemaining == null) {
			this.startRemaining = null;
		} else {
			this.startRemaining.setTime(startRemaining.getTime());
		}
		firePropertyChange("startRemaining", oldValue, this.startRemaining);
	}

	@Override
	public void setTargetFinishDate(Date targetDate) {

		Date oldValue = null;

		if (this.targetFinishDate == null) {
			this.targetFinishDate = new Date();
		} else {
			oldValue = new Date(this.targetFinishDate.getTime());
		}

		if (targetDate == null) {
			this.targetFinishDate = null;
		} else {
			this.targetFinishDate.setTime(targetDate.getTime());
		}
		firePropertyChange("targetFinishDate", oldValue, this.targetFinishDate);
	}

	@Override
	public void setTargetStartDate(Date targetDate) {

		Date oldValue = null;

		if (this.targetStartDate == null) {
			this.targetStartDate = new Date();
		} else {
			oldValue = new Date(this.targetStartDate.getTime());
		}

		if (targetDate == null) {
			this.targetStartDate = null;
		} else {
			this.targetStartDate.setTime(targetDate.getTime());
		}
		firePropertyChange("targetStartDate", oldValue, this.targetStartDate);
	}

	public void setTempId(int tempId) {
		this.tempId = tempId;
	}

	public void setWorkAllocated(int workAllocated) {

		int oldValue = this.workAllocated;
		this.workAllocated = workAllocated;
		firePropertyChange("workAllocated", oldValue, this.workAllocated);

	}

	/**
	 * Sets the work done by the specified resource on the specified date
	 * 
	 * @param workDone
	 * @param resource
	 * @param cal
	 * @throws PersistenceReferenceException
	 */
	public void setWorkDone(int workDone, IndividualResource resource, Date cal) throws PersistenceReferenceException {

		if (resource == null) {
			logger.info("setWorkDone called with null resource");
			return;
		}

		if (cal == null) {
			logger.info("setWorkDone called with null date");
			return;

		}
		PlanDailyProfile profile = getProfile(resource);
		if (profile == null) {
			profile = createProfile(resource);
		}

		PlanProfileEntry ppe = profile.getEntry(cal);
		if (ppe == null) {
			ppe = createEntry(profile, cal, resource);
		}
		// remove the old value from the total
		int oldValue = this.workDone;
		this.workDone = this.workDone - ppe.getWorkDone();
		ppe.setWorkDone(workDone);
		// then add the new value
		this.workDone = this.workDone + ppe.getWorkDone();
		firePropertyChange("workDone", oldValue, this.workDone);
	}

	/**
	 * Sets workRemaining to the value given. <br>
	 * A value of less than zero is forced to zero <br>
	 * <br>
	 * 
	 * <br>{@link #updateWorkRemaining()} is called to plannedFinish the update
	 * 
	 * @param workRemaining
	 *            in minutes
	 */
	@Override
	public void setWorkRemaining(int workRemaining) {

		int newValue = workRemaining < 0 ? 0 : workRemaining;
		if (this.workRemaining == workRemaining) {
			return;
		}

		int oldValue = this.workRemaining;
		this.workRemaining = newValue;
		if (this.workRemaining > 0) {
			setActualFinish(null);
		} else {
			if (this.workRemaining == 0) {
				setActualFinish(new Date());
			}
		}
		firePropertyChange("workRemaining", oldValue, workRemaining);
	}

	/**
	 * If the task has already been started returns {@link #actualStart}<br>
	 * If the task hasn't already started (actualStart == null) returns {@link #startRemaining} if {@link #startRemaining} is also null,
	 * create a {@link #startRemaining} entry of Date(0) and return that
	 * 
	 * @throws PersistenceReferenceException
	 */
	@Override
	public Date start() throws PersistenceReferenceException {

		if (actualStart == null) {
			if (startRemaining == null) {

				startRemaining = new Date();
				startRemaining.setTime(0);
			}
			return startRemaining;
		} else {
			return actualStart;
		}
	}

	// @Override
	// public Date start() throws PersistenceReferenceException {
	// Date d = new Date();
	// d.setTime(startTime());
	// return d;
	// }

	@Override
	public long startTime() throws PersistenceReferenceException {
		return start().getTime();
	}

	public int subTaskCount() {
		return connection.taskService()
				.subTaskCountFor(this);

	}

	/**
	 * Returns a copy of subTasks - the list is a copy but the contained tasks are the same references in the copy as in the original
	 * 
	 * @return
	 */
	public ArrayList<Task> subTasksList() {

		ArrayList<Task> copy = new ArrayList<Task>();
		try {
			copy.addAll(getSubTasks());
		} catch (PersistenceReferenceException e) {
			return copy;
		}
		return copy;
	}

	/**
	 * This retrieves the successors from persistence
	 * 
	 * @return
	 * @throws PersistenceReferenceException
	 */
	@Override
	public List<Task> successors() throws PersistenceReferenceException {
		return connection.taskService()
				.successorsOf(this);
	}

	public String successorsString() throws PersistenceReferenceException {

		StringBuffer buf = new StringBuffer();
		for (int i = 0; i < successors().size(); i++) {
			Task task = successors().get(i);
			if (i != 0) {
				buf.append(",");
			}
			buf.append(task.getTaskId());
		}
		return buf.toString();
	}

	@Override
	public int taskCount() throws PersistenceReferenceException {

		int count = 1; // this task
		for (Task task : getSubTasks()) {
			count = count + task.taskCount();
		}
		return count;
	}

	@Override
	public String toString() {
		return getName();
	}

	/**
	 * The amount of work to do that isn't yet allocated. workRemaining - workAllocated
	 * 
	 * @return (workRemaining - workAllocated)
	 */
	public int unallocatedWork() {

		return workRemaining - workAllocated;
	}

	public void unassignResource() {

		assignedResourceKey = null;

	}

	void workLeftChanged(PlanProfileEntry ppe) {

		if (ppe.getWorkLeft() > workRemaining) {
			setWorkRemaining(ppe.getWorkLeft());
		}
	}

	public boolean isResourcedTask() {
		return resourcedTask;
	}

	public void setResourcedTask(boolean resourcedTask) {
		this.resourcedTask = resourcedTask;
	}

	public void setProjectManager(String pmName) {
		throw new RuntimeException("Not yet implemented");
	}

	public String getProjectManager() {
		throw new RuntimeException("Not yet implemented");
	}

	@Override
	public boolean isService() {
		return taskType == SERVICE;
	}

	public boolean isProjectTask() {
		return (taskType == PROJECT_TASK) || (taskType == PROJECT);
	}

	/**
	 * This task is part of a project if true, part of a service if false
	 * 
	 * @param projectTask
	 */
	public void setProjectTask() {
		this.taskType = PROJECT_TASK;
	}

	@Override
	public boolean isUsingParentPriority() {
		return usingParentPriority;
	}

	@Override
	public void setUsingParentPriority(boolean inheritParentPriority) {
		this.usingParentPriority = inheritParentPriority;
	}

	public int getParentPriority() {
		return parentPriority;
	}

	public void setParentPriority(int parentPriority) {
		this.parentPriority = parentPriority;
	}

	@Override
	public void setWorkDone(int workDone) {
		this.workDone = workDone;
	}

	public boolean isServiceTask() {
		return (taskType == SERVICE) || (taskType == SERVICE_TASK);
	}

	public void setService() {
		taskType = SERVICE;
	}

	public void setServiceTask() {
		taskType = SERVICE_TASK;
	}

}
